package com.bkhech.basicdemo;

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.ffmpeg.global.avutil;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.*;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;

import static org.bytedeco.ffmpeg.global.avutil.AV_LOG_ERROR;
import static org.bytedeco.ffmpeg.global.avutil.av_log_set_level;

/**
 * VideoPicUtil
 *
 * @author guowm
 * @date 2022/11/25
 */
public class VideoUtil {

    static {
        //设置FFmpeg日志级别（默认是debug，设置成error可以屏蔽大多数不必要的控制台消息）
        av_log_set_level(AV_LOG_ERROR);
    }

    public static void main(String[] args) throws Exception {
        long startTime = System.currentTimeMillis();

        String videoUrl = "C:\\Users\\guowm\\Videos\\bilibili-video.mp4";
//        String videoUrl = "C:\\Users\\guowm\\Videos\\111.mp4";
        String targetPath = "C:\\Users\\guowm\\Videos\\";
        String frameFile = targetPath + "bilibili-video-pic-" + System.currentTimeMillis() + ".jpg";
        VideoUtil.grabFrameAndStore(
                videoUrl,
                frameFile,
                "jpg",
                5);
//        String videoSavePath = targetPath + "bilibili-video-covert-" + System.currentTimeMillis() + ".mp4";
//        try {
//            VideoUtil.convert(videoUrl, videoSavePath);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
        long endTime = System.currentTimeMillis();
        System.out.println("耗时：" + (endTime - startTime) + "ms");
    }

    private static final int DEFAULT_BUFFER_SIZE = 8192;


    /**
     * https://zhuanlan.zhihu.com/p/390215359
     * https://www.techwalla.com/articles/video-bitrate-vs-frame-rate
     *
     *
     * 视频相关概念
     * <p>
     * 视频（Video）泛指将一系列静态影像以电信号的方式加以捕捉、记录、处理、储存、传送与重现的各种技术。连续的图像变化每秒超过24帧（frame）画面以上时，根据视觉暂留原理，人眼无法辨别单幅的静态画面；看上去是平滑连续的视觉效果，这样连续的画面叫做视频。
     * 容器中包含：音频、字幕、画面
     * 视频容器格式：3GP、AVI、FLV、MP4、M3U8、MPG、ASF、WMV、MKV、MOV、TS、WebM、MXF。
     * 视频编码格式：H.264/AVC、H.263、 H.263+、H.265、MPEG-1、MPEG-2、MPEG-4、MJPEG、VP8、VP9、Quicktime、RealVideo、Windows Media Video。
     * 音频编码格式：AAC、AC-3、ADPCM、AMR、DSD、MP1、MP2、MP3、PCM、RealAudio、Windows Media Audio。
     * <p>
     * 所以，视频转码转的是编码格式，视频压缩是对视频分辨率、码率、帧率的调整（包括音频）
     * 分辨率：视频的宽*高，常用的有720×480（480p）、1280×720(720p)、1920×1080(1080p)
     * <p>
     * * 经验总结：
     * * 1.如果压缩前的视频时长很短，但是文件很大，比如4秒钟，10m，那么压缩的时候视频的比特率必须要设置很大，才能保证压缩后的视频失真度较少，否则压缩后的视频会很模糊。
     * * 2.如果你用压缩4秒钟，10m文件大小的压缩参数，区压缩1分钟10兆的视频，那么你会发现压缩后的视频竟然变大了！！！！
     * * 这个原因是由于两个文件大小相差不大，但是时长却相差很大，如果你查看这两个视频的比特率信息就会发现。4秒钟的那个文件比特率可能是1分钟那个文件的10倍以上！！
     * * 这样，我们可以得到结论，那就是在压缩的时候，比特率应该是一个变量，而这个变量应该跟时长，原文件比特率相关，这
     * * 里我建议先获得源文件的视频比特率，然后在进行减少其数值，以达到压缩目的。
     *
     * @param inputFile
     * @param outputFile
     * @throws Exception
     */
    public static void convert(String inputFile, String outputFile) throws Exception {
        System.out.println("转码开始");
        Frame capturedFrame;
        try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(inputFile)) {
            grabber.start();

            try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputFile, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels())) {
                /**
                 * ImageWidth：视频宽
                 * ImageHeight：视频高
                 * audioChannels:设置重新编码的音频流中使用的声道数（1 =单声道，2 = 双声道（立体声））。如果未设置任何声道值，则编码器将选择默认值 0。
                 */
//                recorder.setImageWidth(1280);
//                recorder.setImageHeight(720);
                recorder.setFormat("mp4");

                // 视频帧率(保证视频质量的情况下最低25，低于25会出现闪屏)
                // 设置视频帧率（帧率越低，视频会出现断层，越高让人感觉越连续），视频帧率（Frame rate）是用于测量显示帧数的量度。
                // 所谓的测量单位为每秒显示帧数(Frames per Second，简：FPS）或“赫兹”（Hz）
                recorder.setFrameRate(grabber.getFrameRate());
                recorder.setSampleRate(grabber.getSampleRate());
                //视频编码属性配置 H.264 H.265 MPEG
                recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
                //设置视频比特率,单位:b
                recorder.setVideoBitrate(grabber.getVideoBitrate());
                recorder.setAspectRatio(grabber.getAspectRatio());
                // yuv420p,像素
                recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
                //设置视频质量
                recorder.setVideoQuality(avutil.FF_LAMBDA_SHIFT);

                // 设置音频通用编码格式
                recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
                //设置音频比特率,单位:b (比特率越高，清晰度/音质越好，当然文件也就越大 128000 = 182kb)
                recorder.setAudioBitrate(grabber.getAudioBitrate());
                recorder.setAudioOptions(grabber.getAudioOptions());

                recorder.start();
                while (true) {
                    capturedFrame = grabber.grabFrame();
                    if (capturedFrame == null) {
                        System.out.println("转码完成");
                        break;
                    }
                    recorder.record(capturedFrame);
                }
            }
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取视频第几帧
     *
     * @param videoStream 源视频文件路径
     * @param imgSuffix   图片后缀
     * @param skipFrame   截取第几帧的图片
     * @return
     * @throws Exception▏
     */
    public static byte[] getFrame(InputStream videoStream, String imgSuffix, int skipFrame)
            throws Exception {
        final Path path = Files.createTempFile("image_", null);
        final ByteArrayOutputStream baOs = new ByteArrayOutputStream();
        transferTo(videoStream, baOs);
        Files.write(path, baOs.toByteArray());
        byte[] bytes;
        try {
            bytes = grabFrame(path.toFile().getAbsolutePath(), imgSuffix, skipFrame);
        } finally {
            Files.deleteIfExists(path);
        }
        return bytes;
    }

    /**
     * @param videoStream
     * @param out
     * @return
     * @throws IOException
     */
    public static long transferTo(InputStream videoStream, OutputStream out) throws IOException {
        Objects.requireNonNull(out, "out");
        long transferred = 0;
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;
        while ((read = videoStream.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
            out.write(buffer, 0, read);
            transferred += read;
        }
        return transferred;
    }

    /**
     * 获取视频第几帧
     *
     * @param videoFile 源视频文件路径
     * @param imgSuffix 图片后缀
     * @param skipFrame 截取第几帧的图片
     * @return
     * @throws Exception▏
     */
    public static byte[] grabFrame(String videoFile, String imgSuffix, int skipFrame)
            throws Exception {
        if (skipFrame < 0) {
            skipFrame = 0;
        }
        final BufferedImage bufferedImage = getBufferedImage(videoFile, skipFrame);
        try {
            final ByteArrayOutputStream baOs = new ByteArrayOutputStream();
            assert bufferedImage != null;
            ImageIO.write(bufferedImage, imgSuffix, baOs);
            return baOs.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取视频第几帧并保存为图片到指定目录
     *
     * @param videoStream 源视频文件路径
     * @param frameFile   截取帧的图片存放路径
     * @param imgSuffix   图片后缀
     * @param skipFrame   截取第几帧的图片
     * @throws Exception▏
     */
    public static void grabFrameAndStore(InputStream videoStream, String frameFile, String imgSuffix, int skipFrame)
            throws Exception {
        final Path path = Files.createTempFile("image_", null);
        final ByteArrayOutputStream baOs = new ByteArrayOutputStream();
        transferTo(videoStream, baOs);
        Files.write(path, baOs.toByteArray());
        grabFrameAndStore(path.toFile().getAbsolutePath(), frameFile, imgSuffix, skipFrame);
        Files.deleteIfExists(path);
    }

    /**
     * 获取视频第几帧并保存为图片到指定目录
     *
     * @param videoFile 源视频文件路径
     * @param frameFile 截取帧的图片存放路径
     * @param imgSuffix 图片后缀
     * @param skipFrame 截取第几帧的图片
     * @throws Exception▏
     */
    public static void grabFrameAndStore(String videoFile, String frameFile, String imgSuffix, int skipFrame)
            throws Exception {
        if (skipFrame < 0) {
            skipFrame = 0;
        }
        final BufferedImage bufferedImage = getBufferedImage(videoFile, skipFrame);
        File targetFile = new File(frameFile);
        try {
            if (!targetFile.exists()) {
                targetFile.createNewFile();
            }
            assert bufferedImage != null;
            ImageIO.write(bufferedImage, imgSuffix, targetFile);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取视频第几帧并保存为图片到指定目录
     *
     * @param videoFile 源视频文件路径
     * @param skipFrame 截取第几帧的图片
     * @return
     * @throws Exception▏
     */
    private static BufferedImage getBufferedImage(String videoFile, int skipFrame)
            throws Exception {
        if (skipFrame < 0) {
            skipFrame = 0;
        }
        try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoFile);
             Java2DFrameConverter converter = new Java2DFrameConverter()) {
            System.out.println("开始初始化帧抓取器");
            grabber.start();
            //视频帧长度
            int length = grabber.getLengthInFrames();
            if (skipFrame > length) {
                System.err.println("grabFrame failed: " + skipFrame + " frame greater than " + length);
                return null;
            }
            int i = 0;
            while (i < skipFrame) {
                //skip
                grabber.grabFrame();
                i++;
            }

            i = 0;
            Frame frame = null;
            while (i < length - skipFrame) {
                frame = grabber.grabFrame();
                if (frame.image != null) {
                    break;
                }
                i++;
            }

            //对截取的帧进行等比例缩放
            BufferedImage srcBi = converter.getBufferedImage(frame);
            int width = 800;
            int height = (int) (((double) width / srcBi.getWidth()) * srcBi.getHeight());
            BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
            bi.getGraphics().drawImage(srcBi.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
            return bi;
        }
    }

}
